#pragma once

#include <sqlite3.h>
#include <string>
#include <gtest/gtest.h>
#include "generator-support.h"
#include "synchronizability-calc.h"

typedef unsigned int uint;

#define SQLITE_STATUS_CHECK(STATUS, ERRMSG)         \
do                                                  \
  {                                                 \
  if (STATUS != SQLITE_OK &&                        \
      STATUS != SQLITE_ROW &&                       \
      STATUS != SQLITE_DONE)                        \
    {                                               \
    std::ostringstream err;                         \
    err << ERRMSG << sqlite3_errmsg (m_db) << ends; \
    sqlite3_close (m_db);                           \
    cout << err.str () << endl;                     \
    throw runtime_error (err.str ());               \
    }                                               \
  } while (false);


namespace cccn
{
/**
 * The visitor used by the GenerateTidyGraphTemplate_origin_k3 () and GenerateTidyGraphTemplate_improved_k3 () functions
 */
class GenerateTidyGraphEmptyDebugVisitor
  {
  public:
    /// the root graph used to generate graphs in the next iteration
    template <typename G> void RootGraph (G& g) {}
    /// one of the graphs grown from the root graph
    template <typename G> void GrownGraph (G& g) {}
    /// one of the graphs after reconnection is done
    template <typename G> void ReconnectedGraph (G& g) {}
    /// the entire graph queue at the end of each iteration
    template <typename Container> void IterationEnd (Container& queue_g) {}
  };

/**
 * Record the progress of GenerateTidyGraphTemplate_origin_k3 () or 
 * GenerateTidyGraphTemplate_improved_k3 () into a database for further analysis
 *
 * several progres tables are created in a database.
 *  - main-loop-table: record progress of the main iteration loop
 *    - iteration index
 *    - db open time: the time stamp to identify different runs of the program
 *    - out time: the time stamp when the control leaves the iteration
 *    - qeuue out size: the size of the graph queue when the control leaves the iteration
 *    - root graph size: the size of the graph from which new graphs are grown
 *    - root graph girth: the girth of the root graph
 *    - root graph sum-girth: the sum of nodes' girth of the root graph
 *    - root graph avg-dist: the average distance of the root graph
 *
 *  - reconnect-loop-table: record progress of the inner-reconnection-loop
 *    - reconnection index
 *    - iteration index: refer to the iteration index in the main loop
 *    - db open time: the time stamp to identify different runs of the program
 *    - graph size: the size of the reconnected graph
 *    - graph girth: the girth of the reconnected graph
 *    - graph sum-girth: the sum of nodes' girth of the reconnected graph
 *    - graph avg-dist: the average distance of the reconnected graph
 */ 
class GenerateTidyGraphProgressRecorder : public GenerateTidyGraphEmptyDebugVisitor
  {
  public:
    /// close the database connection
    virtual ~GenerateTidyGraphProgressRecorder ();
    /// default constructor
    GenerateTidyGraphProgressRecorder ();
    /**
     * open or create a database connection
     * \param dbname the filename of the database file
     */
    GenerateTidyGraphProgressRecorder (const char* dbname);
    /// open a connection with a database and initialize all attributes
    void OpenDb (const char* dbname = "db.dat");

    /// get date time string from the database
    std::string GetDbDateTime ();

    /// read the properties of a root graph g
    template <typename G> void RootGraph (G& g);
    /// read the properties of a reconnected graph g, and insert a record into reconnection-loop-table
    template <typename G> void ReconnectedGraph (G& g);
    /// read the length of the graph queue, and insert a record into main-loop-table
    template <typename Container> void IterationEnd (Container& queue_g);

  private:
    /// run a SQL statement without returning anything
    void RunSql (const char* sql);

    /// the handle to the database
    sqlite3 *m_db;
    /// the prepared statement for inserting an entry into main_loop_table
    sqlite3_stmt* m_insertMainLoopTableSqlStmt;
    /// the prepared statement for inserting an entry into reconnection_loop_table
    sqlite3_stmt* m_insertReconnectionLoopTableSqlStmt;
    /// filename of a database
    std::string m_dbname;
    /// count the number of iterations already passed
    uint m_iterationIndex;
    /// count the number of reconnection loops already passed in a given iteration loop
    uint m_reconnectionIndex;
  };

template <typename G> void 
GenerateTidyGraphProgressRecorder::RootGraph (G& g)
  {
  const BoostGraph_t& boost_g = g.GetBoostGraph ();

  const char* param = "@graphSize";
  uint status = sqlite3_bind_int (m_insertMainLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertMainLoopTableSqlStmt, param), 
    GraphMeasure (g).NumberOfNodes ());
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");

  param = "@graphGirth";
  status = sqlite3_bind_int (m_insertMainLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertMainLoopTableSqlStmt, param), 
    boost_g[graph_bundle].girth);
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");

  param = "@graphSumgirth";
  status = sqlite3_bind_int (m_insertMainLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertMainLoopTableSqlStmt, param), 
    boost_g[graph_bundle].sumGirth);
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");

  param = "@graphAvgDist";
  status = sqlite3_bind_double (m_insertMainLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertMainLoopTableSqlStmt, param), 
    boost_g[graph_bundle].avgDistance);
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");

  param = "@graphSyncValue";  
  status = sqlite3_bind_double (m_insertMainLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertMainLoopTableSqlStmt, param), 
    boost_g[graph_bundle].syncValue);
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");

  param = "@graphSyncRatio";  
  status = sqlite3_bind_double (m_insertMainLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertMainLoopTableSqlStmt, param), 
    boost_g[graph_bundle].syncRatio);
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");

  ostringstream export_g;
  export_g << g;
  param = "@graphXgmml";
  status = sqlite3_bind_blob (m_insertMainLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertMainLoopTableSqlStmt, param), 
    export_g.str ().c_str (), export_g.str ().size () * sizeof (char), SQLITE_TRANSIENT);
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");
  }

template <typename G> void 
GenerateTidyGraphProgressRecorder::ReconnectedGraph (G& g)
  {        
  const BoostGraph_t& boost_g = g.GetBoostGraph ();

  const char* param = "@graphSize";
  uint status = sqlite3_bind_int (m_insertReconnectionLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertReconnectionLoopTableSqlStmt, param), 
    GraphMeasure (g).NumberOfNodes ());
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");

  param = "@graphGirth";
  status = sqlite3_bind_int (m_insertReconnectionLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertReconnectionLoopTableSqlStmt, param), 
    boost_g[graph_bundle].girth);
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");

  param = "@graphSumgirth";
  status = sqlite3_bind_int (m_insertReconnectionLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertReconnectionLoopTableSqlStmt, param), 
    boost_g[graph_bundle].sumGirth);
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");

  param = "@graphAvgDist";
  status = sqlite3_bind_double (m_insertReconnectionLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertReconnectionLoopTableSqlStmt, param), 
    boost_g[graph_bundle].avgDistance);
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");

  param = "@graphSyncValue";  
  status = sqlite3_bind_double (m_insertReconnectionLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertReconnectionLoopTableSqlStmt, param), 
    boost_g[graph_bundle].syncValue);
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");

  param = "@graphSyncRatio";  
  status = sqlite3_bind_double (m_insertReconnectionLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertReconnectionLoopTableSqlStmt, param), 
    boost_g[graph_bundle].syncRatio);
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");

#if 0
  ostringstream export_g;
  export_g << g;
  param = "@graphXgmml";
  status = sqlite3_bind_blob (m_insertReconnectionLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertReconnectionLoopTableSqlStmt, param), 
    export_g.str ().c_str (), export_g.str ().size () * sizeof (char), SQLITE_TRANSIENT);
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");
#endif

  param = "@iteration";
  status = sqlite3_bind_int (m_insertReconnectionLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertReconnectionLoopTableSqlStmt, param), 
    m_iterationIndex);
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");

  param = "@reconnection";
  status = sqlite3_bind_int (m_insertReconnectionLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertReconnectionLoopTableSqlStmt, param), 
    m_reconnectionIndex);
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");

  status = sqlite3_step (m_insertReconnectionLoopTableSqlStmt);
  SQLITE_STATUS_CHECK (status, "Cannot run the insert SQL statement for the reconnection-loop-table: ");
  status = sqlite3_reset (m_insertReconnectionLoopTableSqlStmt);
  SQLITE_STATUS_CHECK (status, "Cannot reset the insert SQL statement for the reconnection-loop-table: ");

  m_reconnectionIndex++;
  }

template <typename Container> void 
GenerateTidyGraphProgressRecorder::IterationEnd (Container& queue_g)
  {
  const char* param = "@iteration";
  uint status = sqlite3_bind_int (m_insertMainLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertMainLoopTableSqlStmt, param), 
    m_iterationIndex);
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");

  string now = GetDbDateTime ();
  param = "@outTime";
  status = sqlite3_bind_text (m_insertMainLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertMainLoopTableSqlStmt, param), 
    now.c_str (), now.size (), SQLITE_STATIC);
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");

  param = "@outQueueSize";
  status = sqlite3_bind_int (m_insertMainLoopTableSqlStmt, 
    sqlite3_bind_parameter_index (m_insertMainLoopTableSqlStmt, param), 
    queue_g.size ());
  SQLITE_STATUS_CHECK (status, "Cannot bind parameter to " << param << ": ");

  status = sqlite3_step (m_insertMainLoopTableSqlStmt);
  SQLITE_STATUS_CHECK (status, "Cannot run the insert SQL statement for the main-loop-table: ");  
  status = sqlite3_reset (m_insertMainLoopTableSqlStmt);
  SQLITE_STATUS_CHECK (status, "Cannot reset the insert SQL statement for the main-loop-table: ");  

  std::cout << "finished " << m_iterationIndex << "-iteration, queue size=" << queue_g.size ();
  if (!queue_g.empty ())
    {
    std::cout << ", next root_g size=" << GraphMeasure (queue_g.front ()).NumberOfNodes ()
              << ", next root_g girth=" << GetGirth (queue_g.front ());
    }
  std:: cout << std::endl;

  m_iterationIndex++;
  m_reconnectionIndex = 0;  
  }

} // namespace cccn